#!/usr/bin/env python2
#-*- coding: utf-8 -*-

import os
import re
import time
import json
import traceback
import threading

from Ump import utils

from Ump.lib import jsonobject

from Ump.common import exception
from Ump.common import log
from Ump.objs.session_wrapper import enable_log_and_session, _sw, enable_oplog
from Ump.objs.manager_base import Manager
from Ump.objs.cluster.manager import ClusterManager
from Ump.objs.license.manager import LicenseManager
from Ump.objs.db import models
from Ump.objs.db.models import Alert

from Ump.lich.disk import LichDisk, LichDiskParam
from agentapi import disk as disk_api


LOG = log.get_log('Ump.objs.disk.manager')


@models.add_model(models.Disk)
class DiskManager(Manager):
    
    def __init__(self):
        super(DiskManager, self).__init__()
        self.lichDisk = LichDisk()
        self.cluster_manager = ClusterManager()
        self.license_manager = LicenseManager()

    def disk_add(self, kwargs):
        disk_id = kwargs.get('disk_id')

        disk = self._get_one(disk_id)

        return self._disk_add(disk, **kwargs)
            
    def _disk_add(self, disk, is_force=False, is_license_over=False, **kwargs):
        host = disk.host
        pre_stat = disk.stat

        is_add_force =  is_force in ['true', True, 'True']


        #LICH
        cmd = disk_api.DiskAddCmd()
        cmd.host = disk.host.ip
        cmd.target = disk.device
        cmd.target_id = disk.id
        cmd.is_force = is_add_force

        self.taskm.async_post(cmd, callback=self.disk_add_callback)

        disk.update({'stat':'joining'})
        
        return disk 

    @enable_oplog(resource='disk', event='add')
    def disk_add_callback(self, _logger, rsp):
        disk = self._get_one(rsp.target_id)

        _logger.update_props(oplog_obj=disk.device)#, user_id=kwargs.get('op_user_id'))
        _logger.update_props(detail='Add disk to host:%s' % disk.host.ip)

        disk.update({'stat':'OK' })

        self._sync_disk_callback(disk.host, rsp)

        self.utils.exception_pass(self.license_manager.fusionnas_license_sync)

    def _get_one(self, disk_id):
        disk = _sw.get_one(models.Disk, id_or_spec=disk_id) 
        if not disk:
            raise exception.NotFound(disk_id=disk_id)
        return disk

    def disk_delete(self, kwargs):
        disk_id = kwargs.get('disk_id')
        force_del = kwargs.get('is_force')

        disk = self._get_one(disk_id)
        device = disk.device

        cmd = disk_api.DiskDeleteCmd()
        cmd.host = disk.host.ip
        cmd.target = device
        cmd.target_id = disk_id
        cmd.is_force = force_del

        cmd.timeout = 6 * 60 * 60

        self.taskm.async_post(cmd, callback=self.disk_delete_callback)

        values = {
            'pre_stat': disk.stat,
            'stat': 'deleting',
        }
        disk.update(values)
        return disk

    @enable_oplog(resource='disk', event='remove')
    def disk_delete_callback(self, _logger, rsp):
        disk = self._get_one(rsp.target_id)

        _logger.update_props(oplog_obj=disk.device)
        _logger.update_props(detail='Remove disk from host:%s' % disk.host.ip)

        disk.update({'stat':'OK'})

        self._sync_disk_callback(disk.host, rsp)

        self.utils.exception_pass(self.license_manager.fusionnas_license_sync)

    @enable_log_and_session(resource='disk_light', event='start')
    def disk_light_operation(self, _logger, kwargs):
        disk_id = kwargs.get('disk_id')
        option = kwargs.get('option')

        disk = self._get_one(disk_id)

        device = disk.device
        if device and device.startswith('/dev') :
            device = device.strip('[0123456789]')
        
        #OPLOG
        event = 'on' if option == 'start' else 'off'
        oplog_obj = disk.device if disk.device else disk.serial_number
        _logger.update_props(oplog_obj=oplog_obj, event='event', user_id=kwargs.get('op_user_id'))

        #LICH
        param =  LichDiskParam(disk.host.ip)
        stat = self.lichDisk.light(param, option, disk.device, disk.serial_number)
            
        disk.update({'light_stat':event.strip('\n')})
        return True
        
    @enable_log_and_session(resource='raid', event='clean_raid_miss')
    def raid_missing(self, _logger, kwargs):

        succ_hosts, fail_hosts, err = self._raid_missing(**kwargs)

        oplog_obj =  ",".join([succ_hosts, fail_hosts])
        _logger.update_props(oplog_obj=oplog_obj, user_id=kwargs.get('op_user_id'))
        if fail_hosts:
            raise Exception('Success Hosts:%s, Failed Hosts:%s:%s' % (succ_hosts, fail_hosts, err))

    def _raid_missing(self, host_id=None, hostids=[], **kwargs):
        hosts = []
        if host_id:
            host = self._get_one_host(host_id)
            hosts.append(hosts)
        if hostids:
            hosts.extend([self._get_one_host(x) for x in hostids if x])
        if not hosts:
            raise Exception('参数不足')

        fails = []
        succ_hosts = []
        err = ''
        for host in hosts:
            try:
                param =  LichDiskParam(host.ip)
                res = self.lichDisk.raid_missing(param)
                succ_hosts.append(host.ip)
            except Exception, err:
                fails.append(host.ip)
                pass
                
            [disk.update({'raid_missing':''}) for disk in host.disks ]
        fail_hosts = ','.join(fails)
        succ_hosts = ",".join(succ_hosts)
        return succ_hosts, fail_hosts, err

    def _get_one_host(self, host_id):
        host = _sw.get_one(models.Host, id_or_spec=host_id) 
        if not host:
            raise exception.NotFound(host_id=host_id)
        return host

    @enable_log_and_session(resource='raid', event='join')
    def raid_add(self, _logger, kwargs):
        disk_id = kwargs.get('disk_id')
        is_force = kwargs.get('is_force')

        disk = self._get_one(disk_id)
        detail = "%s:%s" % (disk.host.ip, disk.device)

        _logger.update_props(oplog_obj=disk.device, user_id=kwargs.get('op_user_id'), detail=detail)

        return self._raid_add(disk, **kwargs)

    def _raid_add(self, disk, disk_id, is_force=False, **kwargs):
        device = disk.serial_number
        if not disk.serial_number:
            device = disk.device

        cmd = disk_api.RaidAddCmd()
        cmd.host = disk.host.ip
        cmd.target = disk.device
        cmd.target_id = disk.id
        cmd.is_force = (is_force == 'true')

        self.taskm.async_post(cmd, callback=self.raid_add_callback)

        disk.update({'stat':'raid-joining'})
        return disk

    def raid_add_callback(self, rsp):
        disk = self._get_one(rsp.target_id)
        disk.update({'stat':'OK' })
        self._sync_disk_callback(disk.host, rsp)

    @enable_log_and_session(resource='raid', event='clear')
    def raid_delete(self, _logger, kwargs):
        disk_id = kwargs.get('disk_id')
        is_force = kwargs.get('is_force')

        disk = self._get_one(disk_id)
        detail = "%s:%s" % (disk.host.ip, disk.device)

        _logger.update_props(oplog_obj=disk.device, user_id=kwargs.get('op_user_id'), detail=detail)

        return self._raid_delete(disk, **kwargs)

    def _raid_delete(self, disk, is_force=False, **kwargs):
        host = disk.host
        cmd = '%s/lich/admin/node.py --raid_del %s' % (self.lich_home, disk.device)
        if is_force:
            cmd = '%s --force'%cmd
        
        is_event = True
        try:
            res = self._exec_remote(host.ip,cmd)
        except Exception,e:
            if str(e).find("No such file or directory") != -1:
                is_event = False
                pass
            else:
                raise Exception('%s'%e)

        if disk.serial_number and disk.stat != "Failed":
            values = {
                'device': disk.serial_number, 
                'raid': None,
                'raid_cache': None,
                'disk_cache': None
            }
            disk.update(values)
        return True


    def _sync_disk_callback(self, host, rsp):
        tail = rsp.tail
        is_succ = rsp.is_succ
        tier_info = rsp.tier_info
        device_deleting = rsp.device_deleting
        
        if not is_succ:
            return 

        used_disks = []
        _disks = []
        if tail:
            _disks, used_disks = self.get_all_disks(tail, tier_info)

        lich_disks = [x.get('device') for x in used_disks]

        insert_disks = []
        allDisks = []
        for disk in _disks:
            device = disk.get('device')
            if not device:
                continue

            allDisks.append(device)
            if device in device_deleting:
                disk['stat'] = 'deleting'
            else:
                disk['stat'] = 'OK'
                disk['pre_stat'] = disk.get('stat')

            if not isinstance(disk.get('size'), int):
                disk['size'] = utils.str_get_size(disk.get('size', '0'))

            if not isinstance(disk.get('avail'), int):
                disk['avail'] = utils.str_get_size(disk.get('avail', '0'))

            if disk.has_key('partions'):
                disk['partions'] = json.dumps(disk.get('partions'))

            disk['host_id'] =  host.id

            disk_gets = models.Disk.query.filter_by(host_id=host.id, device=device).first()
            serial_number = disk.get("serial_number")
            if not disk_gets and serial_number and disk.get('mode') != 'part':
                disk_gets = _sw.get_one(models.Disk, id_or_spec={'host_id': host.id, 'serial_number': serial_number})

            if disk_gets:
                if disk_gets.stat == 'deleting':
                    keys = ['isjoin', 'used', 'avail', 'size']
                    [disk.pop(key) for key in keys if key in disk.keys()]

                if disk_gets.status == 'stopping':
                    disk.pop('status')

                if disk_gets.stat == 'joining':
                    if self.check_is_joining(host):
                        disk['stat'] = 'joining'

                disk_gets.update(disk)
            else:
                if disk.get('isjoin'):
                    insert_disks.append(disk.get('device'))
                models.Disk(disk).save()

        if insert_disks:
            insert_disks_devices = ','.join(insert_disks)
            detail = '主机%s有新盘%s插入' % (host.ip, insert_disks_devices)
            values = {'detail': detail, 'position': host.ip, 'host_id': host.id, 'level': 'INFO', 'returncode': '0'}

            alert = models.Alert(values).save()
            self.send_message('reload_alert')

        self._delete_disk_not_lich(host, allDisks, is_succ)

        return lich_disks, is_succ

    def _delete_disk_not_lich(self, host, allDisks, issucc):
        for disk in host.disks:
            if not disk.isjoin and disk.stat == 'deleting' :
                continue

            if disk.device not in allDisks and issucc :
                log_msg = 'sync delete disk ：%s(%s-%s-%s)' % (host.ip, disk.id, disk.device, str(allDisks))
                LOG.info(log_msg, extra=self.get_extra)
                disk.delete()

        return None

    def check_is_joining(self, host):
        cmd = "ps aux|grep 'lich.node --disk_add\|node.py --disk_add' |grep -v grep"
        drop_process = ''
        try:
            drop_process = self.api_sync_call(host.ip,cmd)
        except:
            pass
        drop_processes = [x for x in drop_process.split("\n") if x ]
        if drop_processes :
            return True
        return False

    def disk_light_sync(self, host_id):
        host = self._get_one_host(host_id)
        param =  LichDiskParam(host.ip)

        disks = []
        for disk in host.disks:
            stat = 'off'
            device = disk.device
            if disk.device.startswith('/dev'):
                device = disk.device.strip('[0123456789]')

            if disk.adp_name and disk.adp_name != "无":
                try:
                    stat = self.lichDisk.light(param, 'stat', device, disk.serial_number)
                except Exception,e:
                    LOG.info(e)
                    pass
            if 'on' in stat:
                stat = 'on'
            else:
                stat = 'off'
            disk.update({'light_stat':stat.strip('\n')})
            disks.append(disks)
        return disks

    def _get_disk_tier(self, disk_number, tier_info):
        tier = ''
        if disk_number.isdigit():
            tier_info_keys = [str(x) for x in tier_info.keys()]
            tier_info = dict(zip(tier_info.keys(), tier_info.values()))
            tier = tier_info.get(str(disk_number))
        return tier

    def _get_disk_info(self, disk, dev):
        dev_info = dev.get('dev_info', {})
        media_type = dev_info.get('media_type','unknown')
        interface = dev_info.get('interface', '')
        disk['media_type'] = media_type
        disk['interface'] = interface

        size = dev_info.get('size', 0)
        avail = dev_info.get('free', 0)

        disk_stat = dev.get('disk_stat', {})
        used = int(disk_stat.get('used', 0)) * 1024 * 1024
        if disk_stat:
            size = int(disk_stat.get('total', 0)) * 1024 * 1024

        disk.update({'size':size, 'avail':avail, 'used':used})
        return disk

    def _get_raid_info(self, disk, dev):
        dev_info = dev.get('dev_info', {})
        raid_info = dev.get('raid_info', {})

        cache_warn = raid_info.get('cache_warn')
        disk_cache = raid_info.get('disk_cache')
        if not disk_cache:
            disk_cache = dev_info.get('cache')

        raid_cache = raid_info.get('raid_cache')
        stat = raid_info.get('stat', 'OK')
        array = raid_info.get('array')
        inqs = raid_info.get('disk', [])
        bbu_info = raid_info.get('bbu_info')
        raid = raid_info.get('raid')
        adp = raid_info.get('adp')
        adp_name = raid_info.get('adp_name', '')

        raid_missing, failed_disks = self._check_raid_missing(adp_name)

        serial_number = None
        if inqs:
            serial_number = inqs[0]
            disk['serial_number'] = serial_number
        disk.update({'array': array, 'cache_warn': cache_warn, 'bbu_info':json.dumps(bbu_info),\
                     'disk_cache': disk_cache, 'stat': stat,\
                     'raid': raid, 'raid_cache': raid_cache, 'adp': adp, 'adp_name': adp_name, \
                     'raid_missing': raid_missing, 'bbu_info': json.dumps(bbu_info)})
        return disk, failed_disks

    def _check_raid_missing(self, adp_name):
        raid_missing = None
        failed_disks = []
        if adp_name:
            newdisk = re.findall("\([^()]+\)", adp_name)
            if not newdisk == []:
                for new in newdisk:
                    new = new.strip(')').strip('(')
                    renew = re.findall("\[[^()]+\]", new)
                    if not renew == []:
                        renew = eval(renew[0])
                        resnew = [xxx for xxx in renew if xxx and xxx.startswith('/')]
                        nulls = [xxx for xxx in renew if not xxx]
                        if nulls != []:
                            raid_missing = 'raid_missing'
                    failed_disks.extend(resnew)
        return raid_missing, failed_disks

    def get_all_disks(self, tail, tier_info={}):
        res = []
        lichused = []
        failed_disks = [] 
        for key, dev in tail.iteritems():
            if str(key).startswith("nbd"):
                continue

            disk = {}
            disk['mountpoint'] = dev.get('mountpoint')
            adp =  None
            stat = 'OK'

            #TODO
            disk = self._get_disk_info(disk, dev)

            disk_stat = dev.get('disk_stat', {})
            disk_number = disk_stat.get('disk', '').strip()
            disk['disk_number'] = disk_number

            #TODO
            tier = self._get_disk_tier(disk_number, tier_info)
            disk['tier'] = tier

            #TODO
            disk, faileddisks = self._get_raid_info(disk, dev)
            failed_disks.extend(faileddisks)


            dev_info = dev.get('dev_info', {})
            if dev_info.get('fault'):
                disk['stat'] = 'Failed'

            part_info = dev.get('part_info',{})
            partions = part_info.keys()
            path = '%s' % key

            disk.update({'device':path,'partions':partions})

            disk['mode'] = dev.get('mode')
            if dev.get('flag') in ['lich', 'cds', 'mds']:
                lichused.append(disk)
                disk['isjoin'] = False
            else:    #没有加入lich的磁盘
                disk['isjoin'] = True
            if not disk == {}:
                res.append(disk)

        useddisk = [u.get('device') for u in lichused if u.get('device') is not None] 
        realdevices = [u.get('real_device') for u in lichused if u.get('real_device') is not None]  
        realdevices.extend([u.get('real_device') for u in res if u.get('real_device') is not None])
        for new in set(failed_disks):
            if new in useddisk or new in realdevices:
                continue

            disk = {}
            disk.update({'isjoin':True,'device':new,'format':'',\
                        'size':'','free':'','partions':[],'raid':None,\
                        'stat':'Failed', 'media_type':"unknown"})
            res.append(disk)
        return res, lichused


if __name__ == '__main__':
    m = DiskManager()
#    m._sync_disk(1)
#    kw = {u'username': u'admin', u'host_id': None, u'is_force': False, u'disk_id': u'15', u'op_user_id': 1}
    m.sync_disk(2)
